﻿using System;
using Microsoft.Practices.ServiceLocation;

namespace Mockery.Core
{
    /// <summary>
    /// The Mockery acts as the central location for accessing all test related functions which are
    /// Arrange, Act and Assert.
    /// </summary>
    /// <typeparam name="TTestClass">the class under test</typeparam>
    public class Mockery<TTestClass> where TTestClass : class
    {
        #region ctor

        private readonly IArranger _arranger;
        private readonly ITypeCache _typeCache;
        private readonly IActiveMockeryState _activeMockeryState;
        private readonly IDisposableContextState _disposableContextState;

        private IServiceLocator _container;

        /// <summary>
        /// This ctor holds references to test state along with the mocking library reference
        /// </summary>
        /// <param name="arranger">test state coordinator</param>
        /// <param name="typeCache">cached types</param>
        /// <param name="activeMockeryState">the active mockery state</param>
        public Mockery(IArranger arranger, ITypeCache typeCache, 
                       IActiveMockeryState activeMockeryState, IDisposableContextState disposableContextState)
        {
            _arranger = arranger;
            _typeCache = typeCache;
            _activeMockeryState = activeMockeryState;
            _disposableContextState = disposableContextState;
        }

        #endregion

        #region Context Setup and Teardown

        /// <summary>
        /// Sets up context external to what can be mocked/stubbed.  This signature accepts a delegate
        /// that returns an IDisposable instance that will be cleaned up prior to the first assert call.
        /// </summary>
        /// <param name="disposableContextSetup">delegate that returns an IDisposable instance</param>
        /// <returns>Mockery of type TTestClass for fluent style configuration</TTestClass></returns>
        public Mockery<TTestClass> ContextSetup(Func<IDisposable> disposableContextSetup)
        {
            _disposableContextState.Add(disposableContextSetup());

            return this;
        }

        /// <summary>
        /// Sets up context external to what can be mocked/stubbed.  This signature accepts a
        /// simple delegate.
        /// </summary>
        /// <param name="contextSetup">delegate to be run for performing context setup</param>
        /// <returns>Mockery of type TTestClass for fluent style configuration</returns>
        public Mockery<TTestClass> ContextSetup(Action contextSetup)
        {
            contextSetup();

            return this;
        }

        /// <summary>
        /// Runas a simple delegate used for cleaning up test state that cannot be mocked/stubbed
        /// </summary>
        /// <param name="contextTeardown">delegate to be run for performing context setup</param>
        /// <returns>Mockery of type TTestClass for fluent style configuration</returns>
        public Mockery<TTestClass> ContextTeardown(Action contextTeardown)
        {
            contextTeardown();

            return this;
        }

        #endregion

        #region Arrangement

        /// <summary>
        /// Provides the ability to setup the mocks, stubs and static injected types
        /// </summary>
        /// <param name="arrangeAction">the delegate use for arrangement setup</param>
        /// <returns>instance of this class for fluent calling structure</returns>
        public Mockery<TTestClass> Arrange(Action<IArranger> arrangeAction)
        {
            
            // first, we'll set the state to Arrange
            _activeMockeryState.SetState(MockeryState.Arrange);

            arrangeAction(_arranger);

            var builder = _typeCache.RegisterTypes();
            builder.RegisterType<TTestClass>();

            _container = builder.ServiceLocator;

            return this;
        }

        #endregion

        #region Act

        private Exception _thrownException;
        
        /// <summary>
        /// Act is used to run against the object under test. Because the object
        /// is registered as a singleton, multiple Act methods can be run against the
        /// object under test for progressive works.
        /// </summary>
        /// <param name="actionItem">work delegate</param>
        /// <returns>instance of this class for fluent calling structure</returns>
        public Mockery<TTestClass> Act(Action<TTestClass> actionItem)
        {
            _activeMockeryState.SetState(MockeryState.Act);

            try
            {
                actionItem(_container.GetInstance<TTestClass>());
            }
            catch (Exception exception)
            {
                _thrownException = exception;
            }

            return this;
        }

        #endregion

        #region Assert

        /// <summary>
        /// Asserts that an exception was caught on an Act call.  This method must be the first 'Assert' style 
        /// call otherwise an exception saying an unexpected exception was thrown.
        /// </summary>
        /// <typeparam name="TExceptionType">expected exception type</typeparam>
        /// <param name="action">delegate used to check the exception instance state</param>
        /// <returns>instance of this class for fluent calling structure</returns>
        public Mockery<TTestClass> AssertException<TExceptionType>(Action<TExceptionType> action) where TExceptionType : Exception
        {
            
            try
            {
                _activeMockeryState.SetState(MockeryState.Assert);
            }
            catch (Exception exception)
            {
                _thrownException = exception;
            }
            
            
            if (_thrownException is TExceptionType)
                action(_thrownException as TExceptionType);
            else
                throw new MissingExpectedException(string.Format("Exception of type {0} was not thrown",
                                                                 typeof (TExceptionType).Name));
            return this;
        }


        /// <summary>
        /// Assert is used to check a value recorded during the Act calls
        /// </summary>
        /// <param name="actionItem">delegate used for testing</param>
        /// <returns>instance of this class for fluent calling structure</returns>
        public Mockery<TTestClass> Assert(Action actionItem)
        {
            _activeMockeryState.SetState(MockeryState.Assert);

            if (_thrownException != null)
                throw new UnexpectedException(
                    "An exception was caught during an 'Act' method call that was asserted upon.", _thrownException);

            actionItem();

            return this;
        }


        /// <summary>
        /// This assert allows for one of the test types to be retrieved and acted upon in the assert.  This is handy
        /// when using manual mocks
        /// </summary>
        /// <typeparam name="TInjectedType"></typeparam>
        /// <param name="actionItem"></param>
        /// <returns>instance of this class for fluent calling structure</returns>
        public Mockery<TTestClass> Assert<TInjectedType>(Action<TInjectedType> actionItem)
        {
            _activeMockeryState.SetState(MockeryState.Assert);

            if(_thrownException != null)
                throw new UnexpectedException("An exception was caught during an 'Act' method call that was asserted upon.", _thrownException);

            actionItem(_container.GetInstance<TInjectedType>());

            return this;
        }

        /// <summary>
        /// an empty assert that simply forces that the arrangement is validated after Act steps are performed.
        /// </summary>
        /// <returns>instance of this class for fluent calling structure</returns>
        public Mockery<TTestClass> Assert()
        {
            _activeMockeryState.SetState(MockeryState.Assert);

            return this;
        }

        #endregion
    }
}